/*
 * Copyright 2014, NICTA
 *
 * This software may be distributed and modified according to the terms of
 * the BSD 2-Clause license. Note that NO WARRANTY is provided.
 * See "LICENSE_BSD2.txt" for details.
 *
 * @TAG(NICTA_BSD)
 */

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

#include <cspace/cspace.h>

#include <clock/clock.h>
#include <cpio/cpio.h>
#include <nfs/nfs.h>
#include <elf/elf.h>
#include <serial/serial.h>
#include <sos.h>

#include "console.h"
#include "network.h"
#include "elf.h"
#include "frametable.h"
#include "pagetable.h"
#include "picoro.h"
#include "process.h"
#include "syscall.h"
#include "swaptable.h"
#include "task.h"
#include "ut_manager/ut.h"
#include "vfs.h"
#include "vmem_layout.h"
#include "mapping.h"

#include <autoconf.h>

#include "debug.h"
#define verbose SOS_DEBUG_MAIN
#include <sys/debug.h>
#include <sys/panic.h>

#include "macros.h"

/* To differencient between async and and sync IPC, we assign a
 * badge to the async endpoint. The badge that we receive will
 * be the bitwise 'OR' of the async endpoint badge and the badges
 * of all pending notifications. */
#define IRQ_EP_BADGE         (1 << (seL4_BadgeBits - 1))
/* All badged IRQs set high bet, then we use uniq bits to
 * distinguish interrupt sources */
#define IRQ_BADGE_NETWORK (1 << 0)
#define IRQ_BADGE_TIMER   (1 << 1)

#define TTY_NAME             CONFIG_SOS_STARTUP_APP

/* The linker will link this symbol to the start address  *
 * of an archive of attached applications.                */
extern char _cpio_archive[];

const seL4_BootInfo* _boot_info;

seL4_CPtr _sos_ipc_ep_cap;
seL4_CPtr _sos_interrupt_ep_cap;

struct serial *serial;

sos_vfs *vfs;
/**
 * NFS mount point
 */
extern fhandle_t mnt_point;

void handle_syscall(seL4_Word badge, int num_args) {
    int err;
    seL4_Word syscall_number;
    seL4_CPtr reply_cap;
    seL4_IPCBuffer *ipc;
    seL4_Word len_bytes;
    sos_pcb *pcb;

    err = lookup_pcb((int) badge, &pcb);
    if (err != SOS_PROCESS_SUCCESS) {
        dprintf(5, "handle_syscall: unknown PID (%d)\n", (int) badge);
        return;
    }

    syscall_number = seL4_GetMR(0);

    /* Save the caller */
    reply_cap = cspace_save_reply_cap(cur_cspace);
    assert(reply_cap != CSPACE_NULL);

    /* Process system call */
    switch (syscall_number) {

    /* Timer-related syscalls */
    case SOS_SYSCALL_TIME_STAMP :
        sos_syscall_time_stamp(pcb, reply_cap);
        break;

    case SOS_SYSCALL_TIME_USLEEP :
        sos_syscall_usleep(pcb, reply_cap);
        break;

    /* Filesystem-related syscalls */
    case SOS_SYSCALL_OPEN :
        sos_syscall_open(pcb, reply_cap);
        break;

    case SOS_SYSCALL_CLOSE :
        sos_syscall_close(pcb, reply_cap);
        break;

    case SOS_SYSCALL_WRITE :
        sos_syscall_write(pcb, reply_cap);
        break;

    case SOS_SYSCALL_READ :
        sos_syscall_read(pcb, reply_cap);
        break;

    case SOS_SYSCALL_STAT :
        sos_syscall_stat(pcb, reply_cap);
        break;

    case SOS_SYSCALL_GETDIRENT :
        sos_syscall_getdirent(pcb, reply_cap);
        break;

    /* VM-related syscalls */
    case SOS_SYSCALL_BRK :
        sos_syscall_brk(pcb, reply_cap);
        break;

    /* Process management syscalls */
    case SOS_SYSCALL_PID :
        sos_syscall_pid(pcb, reply_cap);
        break;

    case SOS_SYSCALL_PROC_DELETE :
        sos_syscall_process_delete(pcb, reply_cap);
        break;

    case SOS_SYSCALL_PROC_CREATE :
        sos_syscall_process_create(pcb, reply_cap);
        break;

    case SOS_SYSCALL_PROC_WAIT :
        sos_syscall_process_wait(pcb, reply_cap);
        break;

    case SOS_SYSCALL_PROC_STATUS :
        sos_syscall_process_status(pcb, reply_cap);
        break;

    default:
        printf("Unknown syscall %d\n", syscall_number);
        /* we don't want to reply to an unknown syscall */
        cspace_free_slot(cur_cspace, reply_cap);
    }
}

/* Callback passed to the fault handler. */
/* Just wakes up OR destroys the faulting process. */
static void _page_fault_handled(int err, void *cookie) {
    sos_pcb *pcb = (sos_pcb *) cookie;
    seL4_MessageInfo_t reply = seL4_MessageInfo_new(0,0,0,0);
    if (err == SOS_PAGE_TABLE_SUCCESS) {
        // Wake up the process.
        seL4_Send(pcb->reply_cap, reply);
        cspace_free_slot(cur_cspace, pcb->reply_cap);
    } else if (err == SOS_PAGE_TABLE_ABORT) {
        free(pcb);
    } else { // Segfault
        // Process should be torn down.
        dprintf(1, "PID %d: Segfault (access violation or OOM, %d)\n", pcb->pid, err);
        // cspace_free_slot(cur_cspace, pcb->reply_cap);
        process_delete(pcb, NULL, NULL);
    }
}

void *syscall_loop(void *arg) {

    while (1) {
        seL4_Word badge;
        seL4_Word label;
        seL4_MessageInfo_t message;

        message = seL4_Wait(_sos_ipc_ep_cap, &badge);
        label = seL4_MessageInfo_get_label(message);

        dprintf(8, "main: resuming\n");

        if (badge & IRQ_EP_BADGE) {
            /* Interrupt. Badge will be OR'd with each call that's fired since last notify. */
            if (badge & IRQ_BADGE_NETWORK) {
                dprintf(8, "network interrupt\n");
                network_irq();
            }
            if (badge & IRQ_BADGE_TIMER) {
                timer_interrupt();
                // Kernel tick - run low priority tasks
                sos_task_run_high();
                sos_task_run_low();
            }
        } else if (label == seL4_VMFault) {
            /* Page fault */
            int err = 1;
            seL4_CPtr reply_cap;
            if (badge) {
                sos_pcb *pcb = NULL;
                err = lookup_pcb((int) badge, &pcb);
                if (err) {
                    dprintf(1, "main: got message from unknown source (%d)\n",
                            (int) badge);
                    continue;
                } else {
                    dprintf(6, "main: handling %s (PID %d @ %p, %d)\n",
                           seL4_GetMR(2) ? "instruction fault" : "data fault",
                           (int) badge, seL4_GetMR(1), (int) seL4_GetMR(3));
                    reply_cap = cspace_save_reply_cap(cur_cspace);
                    pcb->reply_cap = reply_cap;
                    // Catch permissions faults
                    if (seL4_GetMR(3) & 8) {
                        _page_fault_handled(SOS_PAGE_TABLE_SEGFAULT, (void *) pcb);
                        continue;
                    }
                    err = sos_page_fault_handle(pcb, seL4_GetMR(1),
                              &_page_fault_handled, (void *) pcb);
                    if (err) {
                        // fault unhandleable - segfault
                        dprintf(0, "vm fault at 0x%08x, pc = 0x%08x, %s\n", seL4_GetMR(1),
                                seL4_GetMR(0),
                                seL4_GetMR(2) ? "Instruction Fault" : "Data fault");

                        _page_fault_handled(SOS_PAGE_TABLE_SEGFAULT, (void *) pcb);
                        continue;
                    }
                }
            }
        } else if (label == seL4_NoFault) {
            /* System call */
            dprintf(6, "main: syscall from PID %d\n", badge);
            handle_syscall(badge, seL4_MessageInfo_get_length(message) - 1);
        } else {
            printf("Rootserver got an unknown message\n");
        }

        // Run any high priority tasks created by interrupt / fault handlers
        sos_task_run_high();

        dprintf(8, "main: yielding to processes\n");
    }

    /* Unreachable */
    assert(0);
    return NULL;
}


static void print_bootinfo(const seL4_BootInfo* info) {
    int i;

    /* General info */
    dprintf(1,"Info Page:  %p\n", info);
    dprintf(1,"IPC Buffer: %p\n", info->ipcBuffer);
    dprintf(1,"Node ID: %d (of %d)\n",info->nodeID, info->numNodes);
    dprintf(1,"IOPT levels: %d\n",info->numIOPTLevels);
    dprintf(1,"Init cnode size bits: %d\n", info->initThreadCNodeSizeBits);

    /* Cap details */
    dprintf(1,"\nCap details:\n");
    dprintf(1,"Type              Start      End\n");
    dprintf(1,"Empty             0x%08x 0x%08x\n", info->empty.start, info->empty.end);
    dprintf(1,"Shared frames     0x%08x 0x%08x\n", info->sharedFrames.start,
                                                   info->sharedFrames.end);
    dprintf(1,"User image frames 0x%08x 0x%08x\n", info->userImageFrames.start,
                                                   info->userImageFrames.end);
    dprintf(1,"User image PTs    0x%08x 0x%08x\n", info->userImagePTs.start,
                                                   info->userImagePTs.end);
    dprintf(1,"Untypeds          0x%08x 0x%08x\n", info->untyped.start, info->untyped.end);

    /* Untyped details */
    dprintf(1,"\nUntyped details:\n");
    dprintf(1,"Untyped Slot       Paddr      Bits\n");
    for (i = 0; i < info->untyped.end-info->untyped.start; i++) {
        dprintf(1,"%3d     0x%08x 0x%08x %d\n", i, info->untyped.start + i,
                                                   info->untypedPaddrList[i],
                                                   info->untypedSizeBitsList[i]);
    }

    /* Device untyped details */
    dprintf(1,"\nDevice untyped details:\n");
    dprintf(1,"Untyped Slot       Paddr      Bits\n");
    for (i = 0; i < info->deviceUntyped.end-info->deviceUntyped.start; i++) {
        dprintf(1,"%3d     0x%08x 0x%08x %d\n", i, info->deviceUntyped.start + i,
                                                   info->untypedPaddrList[i + (info->untyped.end - info->untyped.start)],
                                                   info->untypedSizeBitsList[i + (info->untyped.end-info->untyped.start)]);
    }

    dprintf(1,"-----------------------------------------\n\n");

    /* Print cpio data */
    dprintf(1,"Parsing cpio data:\n");
    dprintf(1,"--------------------------------------------------------\n");
    dprintf(1,"| index |        name      |  address   | size (bytes) |\n");
    dprintf(1,"|------------------------------------------------------|\n");
    for(i = 0;; i++) {
        unsigned long size;
        const char *name;
        void *data;

        data = cpio_get_entry(_cpio_archive, i, &name, &size);
        if(data != NULL){
            dprintf(1,"| %3d   | %16s | %p | %12d |\n", i, name, data, size);
        }else{
            break;
        }
    }
    dprintf(1,"--------------------------------------------------------\n");
}

static void _sos_ipc_init(seL4_CPtr* ipc_ep, seL4_CPtr* async_ep){
    seL4_Word ep_addr, aep_addr;
    int err;

    /* Create an Async endpoint for interrupts */
    aep_addr = ut_alloc(seL4_EndpointBits);
    conditional_panic(!aep_addr, "No memory for async endpoint");
    err = cspace_ut_retype_addr(aep_addr,
                                seL4_AsyncEndpointObject,
                                seL4_EndpointBits,
                                cur_cspace,
                                async_ep);
    conditional_panic(err, "Failed to allocate c-slot for Interrupt endpoint");

    /* Bind the Async endpoint to our TCB */
    err = seL4_TCB_BindAEP(seL4_CapInitThreadTCB, *async_ep);
    conditional_panic(err, "Failed to bind ASync EP to TCB");


    /* Create an endpoint for user application IPC */
    ep_addr = ut_alloc(seL4_EndpointBits);
    conditional_panic(!ep_addr, "No memory for endpoint");
    err = cspace_ut_retype_addr(ep_addr,
                                seL4_EndpointObject,
                                seL4_EndpointBits,
                                cur_cspace,
                                ipc_ep);
    conditional_panic(err, "Failed to allocate c-slot for IPC endpoint");
}


static void _sos_init(seL4_CPtr* ipc_ep, seL4_CPtr* async_ep){
    seL4_Word dma_addr;
    seL4_Word low, high;
    int err;

    /* Retrieve boot info from seL4 */
    _boot_info = seL4_GetBootInfo();
    conditional_panic(!_boot_info, "Failed to retrieve boot info\n");
    if(verbose > 0){
        print_bootinfo(_boot_info);
    }

    /* Initialise the untyped sub system and reserve memory for DMA */
    err = ut_table_init(_boot_info);
    conditional_panic(err, "Failed to initialise Untyped Table\n");
    /* DMA uses a large amount of memory that will never be freed */
    dma_addr = ut_steal_mem(DMA_SIZE_BITS);
    conditional_panic(dma_addr == 0, "Failed to reserve DMA memory\n");

    /* find available memory */
    ut_find_memory(&low, &high);

    /* Initialise the untyped memory allocator */
    ut_allocator_init(low, high);

    /* Initialise the cspace manager */
    err = cspace_root_task_bootstrap(ut_alloc, ut_free, ut_translate,
                                     malloc, free);
    conditional_panic(err, "Failed to initialise the c space\n");

    /* Initialise DMA memory */
    err = dma_init(dma_addr, DMA_SIZE_BITS);
    conditional_panic(err, "Failed to intiialise DMA memory\n");

    /* Initialiase other system compenents here */

    _sos_ipc_init(ipc_ep, async_ep);
}

static inline seL4_CPtr badge_irq_ep(seL4_CPtr ep, seL4_Word badge) {
    seL4_CPtr badged_cap = cspace_mint_cap(cur_cspace, cur_cspace, ep, seL4_AllRights, seL4_CapData_Badge_new(badge | IRQ_EP_BADGE));
    conditional_panic(!badged_cap, "Failed to allocate badged cap");
    return badged_cap;
}

/* process_create_cb for when first app starts */
static void _proc_created(int err, int pid, void *cookie) {
    static int first;
    if (err) {
        dprintf(1, "main: failed to create first process (%d)\n", err);
    } else {
        dprintf(5, "main: first process started with PID %d\n", pid);
        // if (!first) {
        //     process_create("tty_test", &_proc_created, NULL);
        //     first = 1;
        // }
    }
}

/*
 * Main entry point - called by crt.
 */
int main(void) {
    int err;

    dprintf(0, "\nSOS Starting...\n");
    _sos_init(&_sos_ipc_ep_cap, &_sos_interrupt_ep_cap);

    /* Initialise the network hardware */
    network_init(badge_irq_ep(_sos_interrupt_ep_cap, IRQ_BADGE_NETWORK));

    /* Initialise libserial */
    // dprintf(0, "\nInitialising libserial...\n");
    // serial = serial_init();

    /* Initialise libclock */
    err = start_timer(badge_irq_ep(_sos_interrupt_ep_cap, IRQ_BADGE_TIMER));
    conditional_panic(err, "Failed to start timer");
    //m1_test();

    /* Initialise global SOS frame table */
    err = frame_table_init();
    conditional_panic(err, "Failed to initialise frame table");
    /* Initialise SOS swap table */
    err = sos_swap_table_init();
    conditional_panic(err, "Failed to initialies swap table");

    /* Initialise global SOS VFS */
    err = sos_vfs_init(&vfs);
    conditional_panic(err, "Failed to initialise SOS global VFS");
    /* Initialise and mount console */
    sos_filesystem *console;
    err = sos_console_init(&console);
    conditional_panic(err, "Failed to initialise console driver");
    dprintf(0, "\nInitialised console driver.\n");
    err = sos_vfs_mount(vfs, "console", console, NULL);
    conditional_panic(err, "Failed to mount console at console");
    dprintf(0, "\nMounted console driver.\n");
    /* Initialise and mount NFS */
    sos_filesystem *nfs;
    err = sos_nfs_init(&nfs);
    conditional_panic(err, "Failed to initialise NFS driver");
    err = sos_vfs_mount(vfs, "", nfs, NULL);
    conditional_panic(err, "Failed to mount NFS at (vfs_root)");
    dprintf(0, "\nMounted NFS driver.\n");

    /* Start the user application */
    process_create(TTY_NAME, &_proc_created, NULL);

    /* Wait on synchronous endpoint for IPC */
    dprintf(0, "\nSOS entering syscall loop\n");
    syscall_loop(NULL);

    /* Not reached */
    return 0;
}
